//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

namespace TridentAPI
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Text;
    using Microsoft.Research.ScientificWorkflow.TridentUtilities;
    using Microsoft.Practices.EnterpriseLibrary.Logging;
    using Er = Microsoft.Research.DataLayer;
    using System.Xml.Serialization;
    using System.Xml;
    using System.ComponentModel;
    
    public static class TypeProviderHelper
    {
        /// <summary>
        /// The registry connection.
        /// </summary>
        private static Er.Connection registryConn;
        
        /// <summary>
        /// The directory name for the temp directory that acts as the holding directory for downloads.
        /// </summary>
        private static string TempDirPath = "TridentTemp";

        /// <summary>
        /// Holds the list of available type initializers.
        /// </summary>
        private static List<Er.TypeInitializer> availableTypeInitializers;

        /// <summary>
        /// Holds the listing of types for which Type providers have already been queried for and not found. This list ensures we dont check again.
        /// </summary>
        private static List<string> nonCustomTypesList = new List<string>();

        /// <summary>
        /// The dictionary of type initializers to the type.
        /// </summary>
        private static Dictionary<string, Type> typeContainer = new Dictionary<string, Type>();

        /// <summary>
        /// This method should be called before the helper can be used
        /// to access the type provider information.
        /// </summary>
        /// <param name="conn"></param>
        public static void Initialize(Er.Connection connection)
        {
            registryConn = connection;
        }

        /// <summary>
        /// Refreshes the type initializers cache. This should be called on import of the type initialzer so that the cache is refreshed.
        /// </summary>
        public static void RefreshTypeInitializersCache()
        {
            if (registryConn != null)
            {
                try
                {
                    if (null != TypeProviderHelper.availableTypeInitializers)
                    {
                        TypeProviderHelper.availableTypeInitializers.Clear();
                    }

                    TypeProviderHelper.nonCustomTypesList.Clear();
                    TypeProviderHelper.availableTypeInitializers = new List<Er.TypeInitializer>(Er.TypeInitializer.CommonSearches.GetAvailableTypeInitializers(registryConn));
                }
                catch (Er.RecordNotFoundException) { }
            }
        }

        /// <summary>
        /// Fetches all the type initializers available in the registry.
        /// </summary>
        /// <returns>
        /// List of available type initializers. If nothing is available, or connection is not set, returns null.
        /// </returns>
        public static List<Er.TypeInitializer> FetchAllTypeInitializers()
        {
            if (null == TypeProviderHelper.availableTypeInitializers)
            {
                TypeProviderHelper.RefreshTypeInitializersCache();
            }

            return TypeProviderHelper.availableTypeInitializers;
        }

        /// <summary>
        /// Fetch the type initializer for the passed type and runtime environment.
        /// </summary>
        /// <param name="type">
        /// The type for which to get the type initializer.
        /// </param>
        /// <param name="hostEnv">
        /// The host environment.
        /// </param>
        /// <returns>
        /// The type initializer for the passed type.
        /// </returns>
        public static Er.TypeInitializer FetchTypeInitializer(string type, Er.Runtime hostEnvironment)
        {
            Er.TypeInitializer typeInitializer = null;
            if (!TypeProviderHelper.nonCustomTypesList.Contains(type))
            {
                List<Er.TypeInitializer> typeInitList = TypeProviderHelper.FetchAllTypeInitializers();
                if (typeInitList != null && typeInitList.Count > 0)
                {
                    typeInitializer = typeInitList.FirstOrDefault(typeInit => typeInit.RelatedType.TypeName.Equals(type, StringComparison.OrdinalIgnoreCase) && typeInit.RuntimeType == hostEnvironment);
                    if (null == typeInitializer)
                    {
                        TypeProviderHelper.nonCustomTypesList.Add(type);
                    }
                }
            }

            return typeInitializer;
        }        
        
        /// <summary>
        /// This method loads the type initalizer assembly and then 
        /// creates an instance of the initializer control.
        /// </summary>
        /// <returns>The base type initializer</returns>
        public static BaseTypeInitializer LoadInitializer(Er.TypeInitializer typeInit, string deserializedValue)
        {
            BaseTypeInitializer baseTypeInit = null;
            if (typeInit != null)
            {
                StringBuilder errorBuilder = new StringBuilder(TridentResourceManager.GetString("CustomTypeLoadFailed"));
                try
                {
                    Type customTypeInitializerNeeded = null;
                    if (!TypeProviderHelper.typeContainer.TryGetValue(typeInit.RelatedType.TypeName, out customTypeInitializerNeeded))
                    {
                        string tempPath = System.IO.Path.Combine(WorkflowUtilities.TempDirPath, TypeProviderHelper.TempDirPath);
                        tempPath = System.IO.Path.Combine(tempPath, Guid.NewGuid().ToString("N"));
                        if (!Directory.Exists(tempPath))
                        {
                            Directory.CreateDirectory(tempPath);
                        }

                        TypeProviderHelper.DownloadCustomInitializerLibraries(typeInit, tempPath);
                        string[] assemblyFiles = Directory.GetFiles(tempPath, "*.dll");
                        string errorMsg = TypeProviderHelper.LoadAssemblies(assemblyFiles);
                        if (string.IsNullOrEmpty(errorMsg))
                        {
                            Assembly[] asms = AppDomain.CurrentDomain.GetAssemblies();
                            foreach (Assembly asm in asms)
                            {
                                customTypeInitializerNeeded = asm.GetType(typeInit.ClassName, false, true);
                                if (customTypeInitializerNeeded != null)
                                {
                                    TypeProviderHelper.typeContainer[typeInit.RelatedType.TypeName] = customTypeInitializerNeeded;
                                    break;
                                }
                            }
                        }
                        else
                        {
                            throw new ArgumentException(errorMsg);
                        }
                    }

                    if (customTypeInitializerNeeded != null)
                    {
                        ConstructorInfo consInfo = customTypeInitializerNeeded.GetConstructor(new Type[] { typeof(string) });
                        if (consInfo != null)
                        {
                            baseTypeInit = consInfo.Invoke(new object[] { deserializedValue }) as BaseTypeInitializer;
                        }
                        else
                        {
                            consInfo = customTypeInitializerNeeded.GetConstructor(new Type[] { });
                            if (consInfo != null)
                            {
                                baseTypeInit = consInfo.Invoke(null) as BaseTypeInitializer;
                            }
                        }
                    }
                }
                catch (IOException ex)
                {
                    TridentErrorHandler.ThrowTridentCustomException(TridentErrorConstants.ErrorNumber4000054, errorBuilder.Append(ex.Message).ToString());
                }
                catch (AccessViolationException ex)
                {
                    TridentErrorHandler.ThrowTridentCustomException(TridentErrorConstants.ErrorNumber4000055, errorBuilder.Append(ex.Message).ToString());
                }
                catch (AppDomainUnloadedException ex)
                {
                    TridentErrorHandler.ThrowTridentCustomException(TridentErrorConstants.ErrorNumber4000056, errorBuilder.Append(ex.Message).ToString());
                }
                catch (BadImageFormatException ex)
                {
                    TridentErrorHandler.ThrowTridentCustomException(TridentErrorConstants.ErrorNumber4000057, errorBuilder.Append(ex.Message).ToString());
                }
                catch (ArgumentException ex)
                {
                    TridentErrorHandler.ThrowTridentCustomException(TridentErrorConstants.ErrorNumber4000058, errorBuilder.Append(ex.Message).ToString());
                }
                catch (TypeLoadException ex)
                {
                    TridentErrorHandler.ThrowTridentCustomException(TridentErrorConstants.ErrorNumber4000059, errorBuilder.Append(ex.Message).ToString());
                }
                catch (MethodAccessException ex)
                {
                    TridentErrorHandler.ThrowTridentCustomException(TridentErrorConstants.ErrorNumber4000060, errorBuilder.Append(ex.Message).ToString());
                }
                catch (MemberAccessException ex)
                {
                    TridentErrorHandler.ThrowTridentCustomException(TridentErrorConstants.ErrorNumber4000061, errorBuilder.Append(ex.Message).ToString());
                }
                catch (TargetInvocationException ex)
                {
                    TridentErrorHandler.ThrowTridentCustomException(TridentErrorConstants.ErrorNumber4000062, errorBuilder.Append(ex.Message).ToString());
                }
                catch (TargetParameterCountException ex)
                {
                    TridentErrorHandler.ThrowTridentCustomException(TridentErrorConstants.ErrorNumber4000063, errorBuilder.Append(ex.Message).ToString());
                }
            }

            return baseTypeInit;
        }

        /// <summary>
        /// Loads the assemblies from the files specified.
        /// </summary>
        /// <param name="typeProvider">
        /// Type Provider into which the assemblies are to be loaded.
        /// </param>
        /// <param name="assemblyFiles">
        /// The files which has to be loaded.
        /// </param>
        /// <returns></returns>
        private static string LoadAssemblies(string[] assemblyFiles)
        {
            #region Validation of Arguments

            if (assemblyFiles == null)
            {
                throw new ArgumentNullException("assemblyFiles");
            }

            #endregion

            StringBuilder errorLoadAssemblies = new StringBuilder();
            foreach (string assemblyFile in assemblyFiles)
            {
                try
                {
                    Assembly depAsm = AssemblyLoad.LoadFrom(assemblyFile);
                }
                catch (ReflectionTypeLoadException rfNotFound)
                {
                    StringBuilder errorString = new StringBuilder();
                    foreach (Exception exp in rfNotFound.LoaderExceptions)
                    {
                        errorString.AppendLine(exp.Message);
                    }

                    Logger.Write(errorString.ToString());
                    errorLoadAssemblies.AppendLine(
                        TridentResourceManager.GetString("LoadAssemblyFailed",
                        assemblyFile,
                        errorString.ToString()));
                }
                catch (Exception exp)
                {
                    Logger.Write(exp.Message);
                    errorLoadAssemblies.AppendLine(
                        TridentResourceManager.GetString("LoadAssemblyFailed",
                        assemblyFile,
                        exp.Message));
                }
            }

            return errorLoadAssemblies.ToString();
        }

        /// <summary>
        /// Download the required dlls for the custom type initializer.
        /// </summary>
        /// <param name="rootActivity">The custom type initializer.</param>
        /// <param name="dllDirectoryPath">Path to be downloaded to.</param>
        public static void DownloadCustomInitializerLibraries(Er.TypeInitializer typeInitializer, string targetFolder)
        {
            // Sort based on name
            typeInitializer.AssemblyPackages.GetEnumerator();
            typeInitializer.AssemblyPackages.Sort((p1, p2) => DateTime.Compare(p2.Updated.When, p1.Updated.When));

            if (typeInitializer.AssemblyPackages.Count > 0)
            {
                Er.AssemblyPackage package = typeInitializer.AssemblyPackages[0];

                // Download the libraries form the activity.
                foreach (Er.AssemblyImage img in package.AssemblyImages)
                {
                    string targetFile = Path.Combine(targetFolder, img.Filename);

                    // if the file not exists in target folder and lib folder then download the file.
                    if (!File.Exists(targetFile) && img.Contents != null)
                    {
                        File.WriteAllBytes(targetFile, img.Contents);
                    }
                }
            }
        }

        /// <summary>
        /// This function is used to deserialize teh value using XmlSerializer.
        /// </summary>
        /// <param name="parameterType">Type of the parameter.</param>
        /// <param name="parameterValue">The parameter value.</param>
        /// <returns>De-serialized object value.</returns>
        public static object FetchDeserializedValue(Type parameterType, string parameterValue)
        {
            object actualVal = null;
            if (parameterType != null)
            {
                if (!TypeDescriptor.GetConverter(parameterType).CanConvertFrom(typeof(string)))
                {
                    try
                    {
                        using (MemoryStream stream = new MemoryStream())
                        {
                            // Write the string to the memory stream.
                            using (StreamWriter tempWriter = new StreamWriter(stream))
                            {
                                tempWriter.Write(parameterValue);
                                tempWriter.Flush();

                                stream.Position = 0;

                                using (XmlReader reader = new XmlTextReader(stream))
                                {
                                    XmlSerializer serializer = new XmlSerializer(parameterType);
                                    if (serializer.CanDeserialize(reader))
                                    {
                                        actualVal = serializer.Deserialize(reader);
                                    }
                                }
                            }
                        }
                    }
                    catch (Exception) { }
                }
            }

            return actualVal;
        }

        /// <summary>
        /// This function is used to retreive the serialize value using XmlSerializer.
        /// </summary>
        /// <param name="parameterType">Type of the parameter.</param>
        /// <param name="parameterValue">The parameter value.</param>
        /// <returns>Serialized string.</returns>
        public static string FetchSerializedValue(Type parameterType, object parameterValue)
        {
            string actualVal = string.Empty;
            if (parameterType != null && !TypeDescriptor.GetConverter(parameterType).CanConvertFrom(typeof(string)))
            {
                try
                {
                    StringBuilder serializedString = new StringBuilder();

                    XmlWriterSettings serializeSetting = new XmlWriterSettings();
                    serializeSetting.OmitXmlDeclaration = true;

                    using (XmlWriter tempWriter = XmlWriter.Create(serializedString, serializeSetting))
                    {
                        XmlSerializer serializer = new XmlSerializer(parameterType);
                        serializer.Serialize(tempWriter, parameterValue);

                        actualVal = serializedString.ToString();
                    }
                }
                catch (Exception) { }
            }
            return actualVal;
        }
    }
}
